Bitmap的加载和Cache(二)— 缓存策略
前言
ImageLoader系列第二篇,也是实现一个ImageLoader的核心。总体思想如下:当程序第一次从网上加载图片后,就将其缓存到设备上,这样下次使用这张图片的时候就不用从网络上重新下载,这样就为用户节省了流量。很多时候为了提高应用的用户体验,往往还会在内存中缓存一份,这样当应用打算显示一张图片,首先会从内存中获取,如果内存中没有就从存储设备获取,存储设备没有,就从网上获取。这是因为从内存中加载图片比从存储设备上加载图片要快,这样不仅提高了程序的效率又为用户节省了不必要的流量开支。
缓存策略包含缓存的添加、获取和删除。添加和获取很好理解,删除是因为缓存的大小是有限制的,当缓存容量满时,程序又要添加缓存,这个时候就要删除已有的缓存。如何定义缓存的新旧就是一种策略,不同的策略对应不同的缓存算法。目前比较常用的缓存算法是LRU(Least Recently Used),即近期最少使用算法。采用LRU算法的缓存有两种:LruCache和DiskLruCache。LruCache用于实现内存缓存,而DiskLruCache用于实现磁盘缓存。
下面就开始慢慢享受精神食粮。
LruCache
LruCache是一个泛型类,它内部采用一个LinkedHashMap以强引用的方式存储外界的缓存对象,其提供了get和put方法来完成缓存的获取和添加。当缓存满时,LruCache会移除较早使用的缓存对象,然后再添加新的缓存对象。关于强弱引用之前也说过,这里再简单说下:
- 强引用:直接的对象引用
- 软引用:当一个对象只有软引用存在时,系统内存不足时此对象会被GC回收
- 弱引用:当一个对象只有弱引用存在时,此对象会随时被GC回收
|
|
以当前应用程序最大内存的1/8作为内存缓存空间。
|
|
DiskLruCache
DiskLruCache的创建
DiskLruCache并不能通过构造方法来创建,它提供了open方法用于创建自身,接口如下:
1public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)四个参数依次表示:缓存目录、版本号、同一个key对应多少个缓存文件和缓存总大小。
缓存目录通常是 /sdcard/Android/data/< application package >/cache,但是我们又要考虑如果这个手机没有SD卡,或者SD卡被移除的情况,因此我们需要专门写一个方法来获取缓存地址。如下:
123456789101112131415161718192021222324252627282930313233private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50; //50Mpublic static final String url = "https://i.loli.net/2017/12/02/5a21fdf88aeef.jpg";/*** 返回一个DiskLruCache对象*/public DiskLruCache open(Context mContext) {DiskLruCache diskLruCache = null;File cacheDir = getDiskCacheDir(mContext, "bitmap");if (!cacheDir.exists()) {cacheDir.mkdirs();}try {diskLruCache = DiskLruCache.open(cacheDir, 1, 1, DISK_CACHE_SIZE);} catch (IOException e) {e.printStackTrace();}return diskLruCache;}/*** 获取缓存目录*/public File getDiskCacheDir(Context context, String uniqueName) {String cachePath;// SD卡存在或者SD卡不可被移除if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) || !Environment.isExternalStorageRemovable()) {// 目录:/sdcard/Android/data/<application package>/cachecachePath = context.getExternalCacheDir().getPath();} else {// 目录:/data/data/<application package>/cachecachePath = context.getCacheDir().getPath();}return new File(cachePath + File.separator + uniqueName);}DiskLruCache的添加
写入操作是借助DiskLruCache.Editor这个类完成的。类似的,这个类也是不能通过new来实例化的,需要调用DiskLruCache的edit()方法来获取实例,接口如下:
1public Editor edit(String key) throws IOException这个方法接受一个参数key,这个key将会作为缓存文件的文件名,并且必须是要和图片的URL一一对应的。因为URL中可能有一些特殊符号,所以不适合用URL来作为key,因此最简单的办法就是用URL的MD5编码作为key。
1234567891011121314151617181920212223242526/*** 将字符串进行MD5编码*/public String hashKeyForDisk(String key) {String cacheKey;try {final MessageDigest mDigest = MessageDigest.getInstance("MD5");mDigest.update(key.getBytes());cacheKey = bytesToHexString(mDigest.digest());} catch (NoSuchAlgorithmException e) {cacheKey = String.valueOf(key.hashCode());}return cacheKey;}public String bytesToHexString(byte[] bytes) {StringBuilder sb = new StringBuilder();for (int i = 0; i < bytes.length; i++) {String hex = Integer.toHexString(0xFF & bytes[i]);if (hex.length() == 1) {sb.append(0);}sb.append(hex);}return sb.toString();}因此,现在我们可以得到一个DiskLruCache.Editor的实例:
123String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg";String key = hashKeyForDisk(imageUrl);DiskLruCache.Editor editor = mDiskLruCache.edit(key);有了DiskLruCache.Editor的实例之后,我们可以调用它的newOutputStream()方法来创建一个输出流,然后把它传入到downloadUrlToStream()中就能实现下载并写入缓存的功能。newOutputStream()方法接受一个index的参数,由于前面在设置valueCount的时候指定的是1,所以这里index传入0就可以了。在写入操作执行完之后,我们还需要调用commit()方法进行提交才能使写入生效,调用about()方法则表示放弃此次写入。不过下载时候需要在非UI线程中执行,所以这里用到了AsyncTask。
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758protected Boolean doInBackground(Object... objects) {DiskLruCacheUtil util = new DiskLruCacheUtil();String key = util.hashKeyForDisk(util.url);DiskLruCache diskLruCache = (DiskLruCache) objects[0];try {DiskLruCache.Editor editor = diskLruCache.edit(key);if (editor != null) {OutputStream outputStream = editor.newOutputStream(0);if (downloadUrlToStream(util.url, outputStream)) {publishProgress("");editor.commit();} else {editor.abort();}}} catch (IOException e) {e.printStackTrace();}return null;}/*** 下载*/private boolean downloadUrlToStream(String urlString, OutputStream outputStream) {HttpURLConnection urlConnection = null;BufferedOutputStream out = null;BufferedInputStream in = null;try {final URL url = new URL(urlString);urlConnection = (HttpURLConnection) url.openConnection();in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE);out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);int b;while ((b = in.read()) != -1) {out.write(b);}return true;} catch (Exception e) {Log.e(TAG, "Error in downloadBitmap - " + e);} finally {if (urlConnection != null) {urlConnection.disconnect();}try {if (out != null) {out.close();}if (in != null) {in.close();}} catch (final IOException e) {}}return false;}执行的时候传入DiskLruCache实例就好了,如下:
12mDiskLruCache = new DiskLruCacheUtil().open(MainActivity.this);new LoadAsyncTask(this).execute(mDiskLruCache);DiskLruCache的读取
读取相对于存储来说就很简单了,主要是借助DiskLruCache的get()方法来实现,接口如下:
1public synchronized Snapshot get(String key) throws IOException传入的参数key就是将URL进行MD5编码后的值了,返回的是一个Snapshot快照对象,然后调用它的getInputStream就得到文件的输入流了,如下:
1234567891011121314private Bitmap getCacheFromDisk() {DiskLruCacheUtil util = new DiskLruCacheUtil();String key = util.hashKeyForDisk(util.url);try {DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);if (snapshot != null) {InputStream in = snapshot.getInputStream(0);return BitmapFactory.decodeStream(in);}} catch (IOException e) {e.printStackTrace();}return null;}
至此,整个DiskLruCache就已经实现完了。
最后
感谢郭神的博客对于DiskLruCache的详细解析,原文:Android DiskLruCache完全解析,硬盘缓存的最佳方案
本篇文章全部代码:https://github.com/Omooo/CacheDemo